// 收集观察者
class Dev {
  constructor() {
    this.watchers = [];
  }
  addWatcher(watcher) {
    this.watchers.push(watcher);
  }
  notify() {
    this.watchers.forEach((watcher) => watcher.update());
  }
}

// 观察者模式---观察者
class Watcher {
  // eg: vm.$watch(vm, 'language.name', () => {})
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    this.oldValue = this.getValue();
  }
  getValue() {
    // new Watcher()时，就会取一次值 vm.$data.language  vm.$data.language.name，取值就会触发对象的 get 方法，因为 data 通过Object.defineProperty() 劫持了
    // new Watcher()时，就会触发 get 方法
    // 将 Watcher 的实例 this 挂载到 Dev 上
    Dev.target = this;
    // 触发 get 方法，将 this 存到观察者队列中
    let value = CompileUtil.getValue(this.expr, this.vm);
    // 释放
    Dev.target = null;
    return value;
  }
  update() {
    const newValue = this.getValue();
    if (newValue !== this.oldValue) {
      this.cb && this.cb(newValue);
    }
  }
}

// 数据劫持的类
class Observer {
  constructor(data) {
    this.observer(data);
  }
  observer(data) {
    if (data && typeof data === "object") {
      for (const key in data) {
        this.definePropertyAction(data, key, data[key]);
      }
    }
  }
  definePropertyAction(obj, key, value) {
    // 新建一个观察者队列，给每个属性加一个观察者
    const dev = new Dev();
    this.observer(value); // 递归
    Object.defineProperty(obj, key, {
      get() {
        // 存入观察者队列
        Dev.target && dev.addWatcher(Dev.target);
        return value;
      },
      set: (newValue) => {
        if (newValue !== value) {
          this.observer(newValue); // 递归
          value = newValue;
          // 值发生变化的时候，执行观察者队列
          dev.notify();
        }
      },
    });
  }
}

// Compiler 编译类
class Compiler {
  constructor(el, vm) {
    this.vm = vm;
    this.el = this.isElement(el) ? el : document.querySelector(el);
    let fragment = this.node2Fragment(this.el);
    this.compile(fragment);
    this.el.appendChild(fragment);
  }
  // 核心编译方法
  compile(fragment) {
    let childNodes = fragment.childNodes; // 返回值是类数组
    [...childNodes].forEach((child) => {
      if (this.isElement(child)) {
        this.compileElement(child);
        this.compile(child);
      } else {
        this.compileText(child);
      }
    });
  }
  // 编译 node
  compileElement(node) {
    let attributes = node.attributes; // 返回值是类数组
    [...attributes].forEach((attr) => {
      const { name, value: expr } = attr; // expr: language.name
      if (this.isDirective(name)) {
        const [, directive] = name.split("-"); // v-model v-on:click
        const [directiveName, eventName] = directive.split(":"); // model on:click
        CompileUtil[directiveName](node, expr, this.vm, eventName);
      }
    });
  }
  // 编译 text
  compileText(node) {
    let content = node.textContent;
    // 正则匹配 {{language.name}}
    if (/\{\{.+?\}\}/.test(content)) {
      CompileUtil["text"](node, content, this.vm);
    }
  }
  // 把节点中的内容移动到fragment文档碎片中
  node2Fragment(node) {
    let fragment = document.createDocumentFragment();
    let first;
    while ((first = node.firstChild)) {
      // 将节点内的元素移动到 fragment 内
      fragment.appendChild(first);
    }
    return fragment;
  }
  // 是不是 vue 指令
  isDirective(directive) {
    return directive.startsWith("v-");
  }
  // 判断节点是不是html元素
  isElement(node) {
    return node.nodeType === 1;
  }
}

// 指令工具类
CompileUtil = {
  on(node, expr, vm, eventName) {
    // expr: change      v-on:click=change
    node.addEventListener(eventName, (e) => {
      vm[expr].call(vm, e);
    });
  },
  html(node, expr, vm) {
    const fn = this.updater.htmlUpdater;
    let value = this.getValue(expr, vm);
    new Watcher(vm, expr, (newValue) => {
      fn(node, newValue);
    });
    fn(node, value);
  },
  text(node, expr, vm) {
    // v-text
    const fn = this.updater.textUpdater;
    let value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      new Watcher(vm, args[1], () => {
        // 这条数据发生变化后，将整条数据，重新计算后赋值
        fn(node, this.getContentValue(expr, vm));
      });
      return this.getValue([...args][1], vm);
    });
    fn(node, value);
  },
  model(node, expr, vm) {
    // v-model
    const fn = this.updater.modelUpdater;
    let value = this.getValue(expr, vm);
    new Watcher(vm, expr, (newValue) => {
      fn(node, newValue);
    });
    fn(node, value);
    node.addEventListener("input", (e) => {
      let { value } = e.target;
      this.setValue(vm, expr, value);
    });
  },
  setValue(vm, expr, value) {
    expr.split(".").reduce((total, item, index, arr) => {
      if (arr.length - 1 === index) {
        return (total[item] = value);
      }
      return total[item];
    }, vm.$data);
  },
  getValue(expr, vm) {
    // expr: language.name
    return expr.split(".").reduce((total, item) => {
      return total[item];
    }, vm.$data);
  },
  getContentValue(expr, vm) {
    return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      return this.getValue([...args][1], vm);
    });
  },
  updater: {
    modelUpdater(node, value) {
      node.value = value;
    },
    textUpdater(node, value) {
      node.textContent = value;
    },
    htmlUpdater(node, value) {
      node.innerHTML = value;
    },
  },
};

// vue 基类
class Vue {
  constructor(options) {
    this.$el = options.el;
    this.$data = options.data;
    const computedObj = options.computed;
    const methods = options.methods;
    if (this.$el) {
      new Observer(this.$data); // 数据劫持
      this.handleComputed(computedObj, this.$data, this); // 处理computed逻辑
      // 将methods方法挂载到vm实例上
      for (const key in methods) {
        Object.defineProperty(this, key, {
          get() {
            return methods[key];
          },
        });
      }
      this.proxyVm(this.$data); // 代理数据，直接通过vm就可以访问
      new Compiler(this.$el, this); // 数据渲染
    }
  }
  handleComputed(computedObj, $data, vm) {
    if (!computedObj) return;
    for (const key in computedObj) {
      Object.defineProperty($data, key, {
        get: () => {
          return computedObj[key].call(vm);
        },
      });
    }
  }
  proxyVm(data) {
    for (const key in data) {
      Object.defineProperty(this, key, {
        get() {
          return data[key];
        },
        set(newVal) {
          data[key] = newVal;
        },
      });
    }
  }
}
